package com.course.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.common.context.UserContextHolder;
import com.common.exception.GlobalException;
import com.common.handler.RedisKeysHandler;
import com.course.domain.dto.CourseCommentDto;
import com.course.domain.dto.SendComment;
import com.course.domain.pojo.CourseComment;
import com.course.domain.pojo.UserInfo;
import com.course.domain.vo.CourseCommentVo;
import com.course.feign.UserFeignClient;
import com.course.handler.RedisTemplateFactory;
import com.course.mapper.CourseCommentMapper;
import com.course.service.ICourseCommentService;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisHashCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;

/**
 * <p>
 * 课程评论表（支持无限级嵌套回复） 服务实现类
 * </p>
 *
 * @author 小杰大人
 * @since 2025-04-15
 */
@Service
@Slf4j
public class CourseCommentServiceImpl extends ServiceImpl<CourseCommentMapper, CourseComment> implements ICourseCommentService {

    @Resource
    UserFeignClient userFeignClient;
    
    @Resource
    ObjectMapper objectMapper;

    @Resource
    TransactionTemplate transactionTemplate;
    
    @Resource
    RedisTemplateFactory redisTemplateFactory;

    /**
     *  评论功能引入缓存的数据结构选项
     *  String：引发BigKey，无法动态添加（每次需要重建缓存），无法操作评论点赞
     *  替换
     *  分片存储 把数据打散分布多处：
     *      1: 元数据层：存储评论总数等统计信息, 只需要统计评论总数使用String结构
     *      2: 评论数据：每一个评论都使用Hash结构进行存储。
     *      3: 一级评论索引: 存储所有的一级评论Key 使用Set结构
     *      4: 二级评论索引: 存储每一个一级评论下的二级评论Key 使用ZSet结构 确保二级评论的有序性
     *  提速：Pipeline 批处理命令
     *       Lua脚本 确保数据的原子性（同时成功，同时失败）
     */
    @Override
    public CourseCommentVo getCourseCommentListByCourseId(Long courseId, Integer sorted) {

        // 缓存重建命令 Pipeline 批处理
        // 1: 元数据层：存储评论总数等统计信息, 只需要统计评论总数使用String结构
        String total = redisTemplateFactory.opsForValue().get(RedisKeysHandler.getCourseCommentMetaKey(courseId));
        // 1.1 判断缓存中是否有 评论总数
        if (StrUtil.isEmpty(total)){
            return CourseCommentVo.createCourseCommentVo();
        }
        // 2:优化： 使用 Pipeline 批处理命令，减少网络开销
        List<Object> list = redisTemplateFactory.executePipelined((RedisCallback<Object>) connection -> {
            // 2: 获取所有的一级评论索引数据 得到所有一级评论的id
            Set<String> parentIds = redisTemplateFactory.opsForSet().members(RedisKeysHandler.getCourseCommentParentIndexKey(courseId));
            // 2.1 判断一级评论集合是否为空 为空表示没有一级评论
            if (parentIds != null) {
                for (String commentId : parentIds) {
                    // 第一组数据：获取一级评论Hash数据
                    connection.hGetAll(RedisKeysHandler.getCourseCommentDataKey(Long.parseLong(commentId)).getBytes());
                    // 第二组数据：获取该评论的子评论ID集合
                    connection.zSetCommands().zRange(
                            RedisKeysHandler.getCourseCommentChildIndexKey(Long.parseLong(commentId)).getBytes(), 0, -1);
                }
            }
            return null;
        });
        /**
         * list中 两两一组 [每一组的第一个为一级评论Hash数据，第二个为一级评论的子评论ID集合]
         */
        List<CourseCommentDto> result = new ArrayList<>(list.size() >>> 1); // 一级评论Hash数据
        Set<Long> userIds = new HashSet<>(list.size());
        for (int i = 0; i < list.size(); i+=2) {
            Object parent = list.get(i);    // 一级评论Hash数据
            // 转换为实体类
            CourseCommentDto parentCourseCommentDto = convertCourseCommentDtoByObject(parent);
            userIds.add(parentCourseCommentDto.getUserId());
            // 得到该一级评论下的二级评论ID集合 类型为LinkedHashSet
            Object childIds = list.get(i + 1);
            if (!(childIds instanceof LinkedHashSet<?>)){
                // 没有二级评论 直接返回
                result.add(parentCourseCommentDto);
                continue;
            }
            LinkedHashSet<String> childIdList = (LinkedHashSet)childIds;
            // 获取每一个二级评论Hash数据
            List<Object> childList = redisTemplateFactory.executePipelined((RedisCallback<Object>) connection -> {
                RedisHashCommands redisHashCommands = connection.hashCommands();
                for (String childId : childIdList) {
                    redisHashCommands.hGetAll(RedisKeysHandler.getCourseCommentDataKey(Long.parseLong(childId)).getBytes(StandardCharsets.UTF_8));
                }
                return null;
            });
            // 构建父子关系
            parentCourseCommentDto.getChildList().addAll(childList.stream().map(o -> {
                        CourseCommentDto courseCommentDto = this.convertCourseCommentDtoByObject(o);
                        if (courseCommentDto.getTargetUserId() != null){
                            userIds.add(courseCommentDto.getTargetUserId());
                        }
                        userIds.add(courseCommentDto.getUserId());
                        return courseCommentDto;
                    })
                    .toList());
            result.add(parentCourseCommentDto);
        }
        // 4: 获取用户信息数据
        Map<Long, UserInfo> userInfoMap = userFeignClient.querySelectUserListByIds(new ArrayList<>(userIds)).getData();
        // 4.1: 构建评论的用户信息
        for (CourseCommentDto parent : result) {
            UserInfo parentUserInfo = userInfoMap.get(parent.getUserId());
            parent.setAvatar(parentUserInfo.getImage());
            parent.setUsername(parentUserInfo.getUsername());
            for (CourseCommentDto child : parent.getChildList()) {
                UserInfo childUserInfo = userInfoMap.get(child.getUserId());
                UserInfo targetUserInfo = userInfoMap.get(child.getTargetUserId());
                child.setAvatar(childUserInfo.getImage());
                child.setUsername(childUserInfo.getUsername());
                child.setTargetUsername(targetUserInfo.getUsername());
            }
        }
        // 5: 根据排序类型进行排序
        if (Objects.equals(sorted, 1)) {  // 最新评论排序 根据发布时间倒序排列
            result.sort((o1, o2) -> o2.getCreateTime().compareTo(o1.getCreateTime()));
        } else {
            // 最热排序，根据点赞数排序
            result.sort((o1, o2) -> o2.getLikeCount().compareTo(o1.getLikeCount()));
        }
        // 5: 返回最终数据
        return CourseCommentVo.createCourseCommentVo(result, Integer.parseInt(total));
    }

    @Override
    public void sendComment(SendComment sendComment) {
        CourseComment comment = new CourseComment();
        // 1: 获取当前发送评论的用户id
        Long usersId = UserContextHolder.getContext().getUsersId();
        // 2: 构建评论信息
        long id;
        try {
            IdentifierGenerator identifierGenerator = new DefaultIdentifierGenerator(InetAddress.getLocalHost());
            id = identifierGenerator.nextId(new Object()).longValue();
        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        }
        comment.setId(id);
        comment.setUserId(usersId);
        comment.setContent(sendComment.getContent());
        comment.setCourseId(sendComment.getCourseId());
        comment.setCreateTime(LocalDateTime.now());
        comment.setRootId(id);
        comment.setLikeCount(0);
        if (sendComment.getType().equals(2)) {
            // 3: 回复某个用户
            comment.setTargetUserId(sendComment.getReplyUserId());
            comment.setParentId(sendComment.getCommentId());
            comment.setRootId(sendComment.getCommentId());
        }
        transactionTemplate.executeWithoutResult(status -> {
            // 新增评论数据
            boolean save = save(comment);
            // 新增成功后 建立缓存数据
            if (BooleanUtil.isTrue(save)){
                insertCommentCache(comment);
            }
        });
        // TODO: 2025/4/22 : 回复用户：通过异步队列 通知对应用户
    }

    @Override
    public void likeComment(Long commentId) {
        // 1：获取当前用户id
        Long usersId = UserContextHolder.getContext().getUsersId();
        // 2：获取当前的评论信息
        CourseComment comment = this.getById(commentId);
        if (comment == null){
            throw new GlobalException("当前评论不存在");
        }
        // 3：判断当前用户是否已经点赞
        Boolean member = redisTemplateFactory.opsForSet().isMember(RedisKeysHandler.getCourseCommentLikeListKey(commentId), usersId.toString());
        // 4: 已点赞 -- 执行取消点赞
        if (BooleanUtil.isTrue(member)){
            transactionTemplate.executeWithoutResult(status -> {
                //4.1: 数据库点赞数量 - 1
                boolean updated = this.update().setSql("like_count = like_count - 1").eq("id", commentId).update();
                if (updated){
                    // 4.2: Redis移除当前用户点赞
                    // 4.3: Redis缓存评论点赞数量 - 1
                    redisTemplateFactory.executePipelined((RedisCallback<Object>) connection -> {
                        //4.2: Redis移除当前用户点赞
                        connection.setCommands().sRem(
                                RedisKeysHandler.getCourseCommentLikeListKey(commentId).getBytes(StandardCharsets.UTF_8),
                                String.valueOf(usersId).getBytes(StandardCharsets.UTF_8)
                        );
                        //4.3: Redis缓存评论点赞数量 - 1
                        connection.hashCommands().hIncrBy(
                                RedisKeysHandler.getCourseCommentDataKey(commentId).getBytes(StandardCharsets.UTF_8),
                                "likeCount".getBytes(StandardCharsets.UTF_8),
                                -1L
                        );
                        return null;
                    });
                }
            });
        }else {
            transactionTemplate.executeWithoutResult(status -> {
                // 5: 未点赞 -- 执行点赞逻辑
                // 5.1: 数据库点赞数量 + 1
                boolean updated = this.update().setSql("like_count = like_count + 1").eq("id", commentId).update();
                if (updated){
                    // 5.2: Redis缓存当前用户点赞
                    // 5.3: Redis缓存评论点赞数量 + 1
                    redisTemplateFactory.executePipelined((RedisCallback<Object>) connection -> {
                        // 5.2: Redis缓存当前用户点赞
                        connection.setCommands().sAdd(
                                RedisKeysHandler.getCourseCommentLikeListKey(commentId).getBytes(StandardCharsets.UTF_8),
                                String.valueOf(usersId).getBytes(StandardCharsets.UTF_8)
                        );
                        // 5.3: Redis缓存评论点赞数量 + 1
                        connection.hashCommands().hIncrBy(
                                RedisKeysHandler.getCourseCommentDataKey(commentId).getBytes(StandardCharsets.UTF_8),
                                "likeCount".getBytes(StandardCharsets.UTF_8),
                                1L
                        );
                        return null;
                    });
                }
            });
        }
    }

    @Override
    public void deleteComment(Long courseId) {
        // 1: 删除当前课程id下的所有评论
        transactionTemplate.executeWithoutResult(status -> {
            boolean remove = this.remove(new LambdaQueryWrapper<CourseComment>().eq(CourseComment::getCourseId, courseId));
            if (remove){
                // 3：删除缓存评论数据
                // 4：删除缓存一级索引
                // 5：删除缓存二级索引
                redisTemplateFactory.executePipelined((RedisCallback<?>) connection -> {
                    // 2：删除缓存元数据
                    connection.del(RedisKeysHandler.getCourseCommentMetaKey(courseId).getBytes(StandardCharsets.UTF_8));
                    // 3：获取所有的一级评论id
                    Set<String> parentId = redisTemplateFactory.opsForSet().members(RedisKeysHandler.getCourseCommentParentIndexKey(courseId));
                    if (parentId != null) {
                        for (String pid : parentId) {
                            // 3.1: 删除一级评论数据
                            connection.del(RedisKeysHandler.getCourseCommentDataKey(Long.parseLong(pid)).getBytes(StandardCharsets.UTF_8));
                            // 4: 获取所有的二级评论id
                            Set<String> childId = redisTemplateFactory.opsForZSet().range(RedisKeysHandler.getCourseCommentChildIndexKey(Long.parseLong(pid)), 0, -1);
                            if (childId != null) {
                                for (String cid : childId) {
                                    // 4.1: 删除二级评论数据
                                    connection.del(RedisKeysHandler.getCourseCommentDataKey(Long.parseLong(cid)).getBytes(StandardCharsets.UTF_8));
                                }
                            }
                        }
                    }
                    return null;
                });
            }
        });
    }

    /**
     * 插入评论缓存 异步执行
     * @param comment 评论数据
     *  1: 新增评论数据 HSET KEY Map<FIELD VALUE>
     *  2: 新增评论层级数据 SADD KEY VALUE or ZADD KEY SCORE VALUE
     *  3: 评论总数 + 1 INCR key
     *  保原子性 采用Lua脚本进行执行 , 确保所有数据都为字符串，MasterRedisTemplate的序列化器全是stringRedisSerializer
     *  KEYS[1]: 评论数据Hash键，KEYS[2]: 一级评论集合键，KEYS[3]: 二级评论集合键，KEYS[4]: 评论总数键
     *  ARGV[1]: 评论ID，ARGV[2]: 父评论ID (空字符串表示一级评论)，
     *  ARGV[3]...N: 交替的field和value (field1, value1, field2, value2,...)，
     *  ARGV[4]: 时间戳(用于ZSet排序)
     */
    @Async
    public void insertCommentCache(CourseComment comment) {
        // 1：执行Lua脚本
        ClassPathResource classPathResource = new ClassPathResource("/script/insertCourseComment.lua");
        DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
        defaultRedisScript.setLocation(classPathResource);  // 脚本资源地址
        defaultRedisScript.setResultType(Long.class);    // 指定脚本执行结果
        // 构建脚本需要的Key KEYS[1]: 评论数据Hash键，KEYS[2]: 一级评论集合键，KEYS[3]: 二级评论集合键，KEYS[4]: 评论总数键
        List<String> keys = Arrays.asList(
                RedisKeysHandler.getCourseCommentDataKey(comment.getId()),  // KEYS[1]: 评论数据Hash键
                RedisKeysHandler.getCourseCommentParentIndexKey(comment.getCourseId()), // KEYS[2]: 一级评论集合键
                RedisKeysHandler.getCourseCommentChildIndexKey(comment.getParentId()),  // KEYS[3]: 二级评论集合键
                RedisKeysHandler.getCourseCommentMetaKey(comment.getCourseId()) // KEYS[4]: 评论总数键
        );
        // 构建脚本的ARGV ARGV[1]: 评论ID，ARGV[2]: 父评论ID (null字符串表示一级评论,借助String.valueOf特性)，ARGV[3]...N: 交替的field和value (field1, value1, field2, value2,...)，ARGV[4]: 时间戳(用于ZSet排序)
        Map<String,Object> map = new HashMap<>();
        BeanUtil.beanToMap(comment, map, CopyOptions.create().setFieldValueEditor((name, value) -> String.valueOf(value)));
        Object[] argv = new Object[1 + 1 + (map.size() << 1) + 1];
        argv[0] = String.valueOf(comment.getId());  // ARGV[1]: 评论ID
        argv[1] = comment.getParentId() == null ? "-1" : comment.getParentId().toString(); // ARGV[2]: 父评论ID (-1表示一级评论)
        int i = 2;
        for (Map.Entry<String, Object> entry : map.entrySet()) {    // ARGV[3]...N: 交替的field和value (field1, value1, field2, value2,...)
            argv[i++] = entry.getKey();
            argv[i++] = String.valueOf(entry.getValue());
        }
        argv[i] = String.valueOf(comment.getCreateTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());  // ARGV[4]: 时间戳(用于ZSet排序)
        Long result = redisTemplateFactory.executeScript(defaultRedisScript, keys, argv);
        if (Objects.equals(result,1L)){
            log.info("本次评论缓存建立成功 评论Id：{}，评论内容：{}，评论作者：{}", comment.getId(), comment.getContent(), comment.getUserId());
        }else {
            log.error("本次评论缓存建立失败 评论Id：{}，评论内容：{}，评论作者：{}", comment.getId(), comment.getContent(), comment.getUserId());
        }
    }

    public CourseCommentDto convertCourseCommentDtoByObject(Object o){
        CourseCommentDto bean = objectMapper.convertValue(o, CourseCommentDto.class);
        bean.setChildList(new ArrayList<>());
        return bean;
    }
}
